/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.apps.dashclock.api;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import java.net.URISyntaxException;
/**
* A parcelable, serializable object representing data related to a {@link DashClockExtension} that
* should be shown to the user.
*
* <p>
* This class follows the <a href="http://en.wikipedia.org/wiki/Fluent_interface">fluent
* interface</a> style, using method chaining to provide for more readable code. For example, to set
* the status and visibility of this data, use {@link #status(String)} and {@link #visible(boolean)}
* methods like so:
*
* <pre class="prettyprint">
* ExtensionData data = new ExtensionData();
* data.visible(true).status("hello");
* </pre>
*
* Conversely, to get the status, use {@link #status()}. Setters and getters are thus overloads
* (or overlords?) of the same method.
*
* <h3>Required fields</h3>
*
* While no fields are required, if the data is 'visible' (i.e. {@link #visible(boolean)} has been
* called with <code>true</code>, at least the following fields should be populated:
*
* <ul>
* <li>{@link #icon(int)}</li>
* <li>{@link #status(String)}</li>
* </ul>
*
* Really awesome extensions will also set these fields:
*
* <ul>
* <li>{@link #expandedTitle(String)}</li>
* <li>{@link #expandedBody(String)}</li>
* <li>{@link #clickIntent(android.content.Intent)}</li>
* </ul>
*
* @see DashClockExtension#publishUpdate(ExtensionData)
*/
public class ExtensionData implements Parcelable {
/**
* Since there might be a case where new versions of DashClock use extensions running
* old versions of the protocol (and thus old versions of this class), we need a versioning
* system for the parcels sent between the core app and its extensions.
*/
public static final int PARCELABLE_VERSION = 1;
/**
* The number of fields in this version of the parcelable.
*/
public static final int PARCELABLE_SIZE = 6;
private static final String KEY_VISIBLE = "visible";
private static final String KEY_ICON = "icon";
private static final String KEY_STATUS = "status";
private static final String KEY_EXPANDED_TITLE = "title";
private static final String KEY_EXPANDED_BODY = "body";
private static final String KEY_CLICK_INTENT = "click_intent";
/**
* The maximum length for {@link #status(String)}. Enforced by {@link #clean()}.
*/
public static final int MAX_STATUS_LENGTH = 32;
/**
* The maximum length for {@link #expandedTitle(String)}. Enforced by {@link #clean()}.
*/
public static final int MAX_EXPANDED_TITLE_LENGTH = 100;
/**
* The maximum length for {@link #expandedBody(String)}. Enforced by {@link #clean()}.
*/
public static final int MAX_EXPANDED_BODY_LENGTH = 1000;
private boolean mVisible = false;
private int mIcon = 0;
private String mStatus = null;
private String mExpandedTitle = null;
private String mExpandedBody = null;
private Intent mClickIntent = null;
public ExtensionData() {
}
/**
* Returns whether or not the relevant extension should be visible (whether or not there is
* relevant information to show to the user about the extension). Default false.
*/
public boolean visible() {
return mVisible;
}
/**
* Sets whether or not the relevant extension should be visible (whether or not there is
* relevant information to show to the user about the extension). Default false.
*/
public ExtensionData visible(boolean visible) {
mVisible = visible;
return this;
}
/**
* Returns the ID of the resource within the extension's package that represents this
* data. Default 0.
*/
public int icon() {
return mIcon;
}
/**
* Sets the ID of the resource within the extension's package that represents this
* data. The icon should be entirely white, with alpha, and about 48x48 dp. It will be
* scaled down as needed. If there is no contextual icon representation of the data, simply
* use the extension or app icon. Default 0.
*/
public ExtensionData icon(int icon) {
mIcon = icon;
return this;
}
/**
* Returns the short string representing this data, to be shown in DashClock's collapsed form.
* Default null.
*/
public String status() {
return mStatus;
}
/**
* Sets the short string representing this data, to be shown in DashClock's collapsed form.
* Should be no longer than a few characters. For example, if your {@link #expandedTitle()} is
* "45°, Sunny", your status could simply be "45°". Alternatively, if the status contains a
* single newline, DashClock may break it up over two lines and use a smaller font. This should
* be avoided where possible in favor of an {@link #expandedTitle(String)}. Default null.
*/
public ExtensionData status(String status) {
mStatus = status;
return this;
}
/**
* Returns the expanded title representing this data. Generally a longer form of
* {@link #status()}. Default null.
*/
public String expandedTitle() {
return mExpandedTitle;
}
/**
* Sets the expanded title representing this data. Generally a longer form of
* {@link #status()}. Can be multiple lines, although DashClock will cap the number of lines
* shown. If this is not set, DashClock will just use the {@link #status()}.
* Default null.
*/
public ExtensionData expandedTitle(String expandedTitle) {
mExpandedTitle = expandedTitle;
return this;
}
/**
* Returns the expanded body text representing this data. Default null.
*/
public String expandedBody() {
return mExpandedBody;
}
/**
* Sets the expanded body text (below the expanded title), representing this data. Can span
* multiple lines, although DashClock will cap the number of lines shown. Default null.
* @param expandedBody
* @return
*/
public ExtensionData expandedBody(String expandedBody) {
mExpandedBody = expandedBody;
return this;
}
/**
* Returns the click intent to start (using
* {@link android.content.Context#startActivity(android.content.Intent)}) when the user clicks
* the status in DashClock. Default null.
*/
public Intent clickIntent() {
return mClickIntent;
}
/**
* Sets the click intent to start (using
* {@link android.content.Context#startActivity(android.content.Intent)}) when the user clicks
* the status in DashClock. The activity represented by this intent will be started in a new
* task and should be exported. Default null.
*/
public ExtensionData clickIntent(Intent clickIntent) {
mClickIntent = clickIntent;
return this;
}
/**
* Serializes the contents of this object to JSON.
*/
public JSONObject serialize() throws JSONException {
JSONObject data = new JSONObject();
data.put(KEY_VISIBLE, mVisible);
data.put(KEY_ICON, mIcon);
data.put(KEY_STATUS, mStatus);
data.put(KEY_EXPANDED_TITLE, mExpandedTitle);
data.put(KEY_EXPANDED_BODY, mExpandedBody);
data.put(KEY_CLICK_INTENT, (mClickIntent == null) ? null : mClickIntent.toUri(0));
return data;
}
/**
* Deserializes the given JSON representation of extension data, populating this
* object.
*/
public void deserialize(JSONObject data) throws JSONException {
this.mVisible = data.optBoolean(KEY_VISIBLE);
this.mIcon = data.optInt(KEY_ICON);
this.mStatus = data.optString(KEY_STATUS);
this.mExpandedTitle = data.optString(KEY_EXPANDED_TITLE);
this.mExpandedBody = data.optString(KEY_EXPANDED_BODY);
try {
this.mClickIntent = Intent.parseUri(data.optString(KEY_CLICK_INTENT), 0);
} catch (URISyntaxException ignored) {
}
}
/**
* Serializes the contents of this object to a {@link Bundle}.
*/
public Bundle toBundle() {
Bundle data = new Bundle();
data.putBoolean(KEY_VISIBLE, mVisible);
data.putInt(KEY_ICON, mIcon);
data.putString(KEY_STATUS, mStatus);
data.putString(KEY_EXPANDED_TITLE, mExpandedTitle);
data.putString(KEY_EXPANDED_BODY, mExpandedBody);
data.putString(KEY_CLICK_INTENT, (mClickIntent == null) ? null : mClickIntent.toUri(0));
return data;
}
/**
* Deserializes the given {@link Bundle} representation of extension data, populating this
* object.
*/
public void fromBundle(Bundle src) {
this.mVisible = src.getBoolean(KEY_VISIBLE, true);
this.mIcon = src.getInt(KEY_ICON);
this.mStatus = src.getString(KEY_STATUS);
this.mExpandedTitle = src.getString(KEY_EXPANDED_TITLE);
this.mExpandedBody = src.getString(KEY_EXPANDED_BODY);
try {
this.mClickIntent = Intent.parseUri(src.getString(KEY_CLICK_INTENT), 0);
} catch (URISyntaxException ignored) {
}
}
/**
* @see Parcelable
*/
public static final Creator<ExtensionData> CREATOR
= new Creator<ExtensionData>() {
public ExtensionData createFromParcel(Parcel in) {
return new ExtensionData(in);
}
public ExtensionData[] newArray(int size) {
return new ExtensionData[size];
}
};
private ExtensionData(Parcel in) {
int parcelableVersion = in.readInt();
int parcelableSize = in.readInt();
// Version 1 below
if (parcelableVersion >= 1) {
this.mVisible = (in.readInt() != 0);
this.mIcon = in.readInt();
this.mStatus = in.readString();
if (TextUtils.isEmpty(this.mStatus)) {
this.mStatus = null;
}
this.mExpandedTitle = in.readString();
if (TextUtils.isEmpty(this.mExpandedTitle)) {
this.mExpandedTitle = null;
}
this.mExpandedBody = in.readString();
if (TextUtils.isEmpty(this.mExpandedBody)) {
this.mExpandedBody = null;
}
try {
this.mClickIntent = Intent.parseUri(in.readString(), 0);
} catch (URISyntaxException ignored) {
}
}
// Version 2 below
// Skip any fields we don't know about. For example, if our current version's
// PARCELABLE_SIZE is 6 and the input parcelableSize is 12, skip the 6 fields we
// haven't read yet (from above) since we don't know about them.
in.setDataPosition(in.dataPosition() + (PARCELABLE_SIZE - parcelableSize));
}
@Override
public void writeToParcel(Parcel parcel, int i) {
/**
* NOTE: When adding fields in the process of updating this API, make sure to bump
* {@link #PARCELABLE_VERSION} and modify {@link #PARCELABLE_SIZE}.
*/
parcel.writeInt(PARCELABLE_VERSION);
parcel.writeInt(PARCELABLE_SIZE);
// Version 1 below
parcel.writeInt(mVisible ? 1 : 0);
parcel.writeInt(mIcon);
parcel.writeString(TextUtils.isEmpty(mStatus) ? "" : mStatus);
parcel.writeString(TextUtils.isEmpty(mExpandedTitle) ? "" : mExpandedTitle);
parcel.writeString(TextUtils.isEmpty(mExpandedBody) ? "" : mExpandedBody);
parcel.writeString((mClickIntent == null) ? "" : mClickIntent.toUri(0));
// Version 2 below
}
@Override
public int describeContents() {
return 0;
}
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
try {
ExtensionData other = (ExtensionData) o;
return other.mVisible == mVisible
&& other.mIcon == mIcon
&& TextUtils.equals(other.mStatus, mStatus)
&& TextUtils.equals(other.mExpandedTitle, mExpandedTitle)
&& TextUtils.equals(other.mExpandedBody, mExpandedBody)
&& intentEquals(other.mClickIntent, mClickIntent);
} catch (ClassCastException e) {
return false;
}
}
private static boolean intentEquals(Intent x, Intent y) {
if (x == null || y == null) {
return x == y;
} else {
return x.equals(y);
}
}
/**
* Returns true if the two provided data objects are equal (or both null).
*/
public static boolean equals(ExtensionData x, ExtensionData y) {
if (x == null || y == null) {
return x == y;
} else {
return x.equals(y);
}
}
/**
* Cleans up this object's data according to the size limits described by
* {@link #MAX_STATUS_LENGTH}, {@link #MAX_EXPANDED_TITLE_LENGTH}, etc.
*/
public void clean() {
if (!TextUtils.isEmpty(mStatus) && mStatus.length() > MAX_STATUS_LENGTH) {
mStatus = mStatus.substring(0, MAX_STATUS_LENGTH);
}
if (!TextUtils.isEmpty(mExpandedTitle) && mStatus.length() > MAX_EXPANDED_TITLE_LENGTH) {
mExpandedTitle = mExpandedTitle.substring(0, MAX_EXPANDED_TITLE_LENGTH);
}
if (!TextUtils.isEmpty(mExpandedBody) && mStatus.length() > MAX_EXPANDED_BODY_LENGTH) {
mExpandedBody = mExpandedBody.substring(0, MAX_EXPANDED_BODY_LENGTH);
}
}
}